Skip to content

WIP: Add DeepSeek Anthropic-compatible route#3449

Merged
Hmbown merged 1 commit into
mainfrom
issue/2963-deepseek-anthropic-route
Jun 23, 2026
Merged

WIP: Add DeepSeek Anthropic-compatible route#3449
Hmbown merged 1 commit into
mainfrom
issue/2963-deepseek-anthropic-route

Conversation

@Hmbown

@Hmbown Hmbown commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Goal

Add a first-class opt-in route for DeepSeek's Anthropic-compatible API without changing the existing DeepSeek Chat Completions path.

Refs #2963.

Changes

  • Adds deepseek-anthropic / deepseek_anthropic provider metadata, config tables, aliases, defaults, and env var wiring.
  • Defaults the route to https://api.deepseek.com/anthropic with deepseek-v4-pro.
  • Routes the provider through the existing Anthropic Messages adapter and x-api-key / anthropic-version header dialect.
  • Marks runtime capability metadata as AnthropicMessages, while keeping DeepSeek-native cache telemetry scoped to the existing DeepSeek routes.
  • Updates provider docs, provider registry drift checks, and generated web facts maps for the new manual provider.
  • Adds focused config, URL, header, capability, and metadata parity tests.

Upstream Docs Checked

Verification

  • cargo fmt --all -- --check
  • cargo test -p codewhale-config --locked provider_metadata_defaults_match_runtime_helpers
  • cargo test -p codewhale-config --locked deepseek_anthropic_route_defaults_to_anthropic_endpoint
  • cargo test -p codewhale-tui --bin codewhale-tui --locked messages_url_tolerates_v1_suffix
  • cargo test -p codewhale-tui --bin codewhale-tui --locked deepseek_anthropic_uses_anthropic_header_dialect
  • cargo test -p codewhale-tui --bin codewhale-tui --locked provider_capability_deepseek_anthropic_uses_messages_payload
  • cargo test -p codewhale-tui --bin codewhale-tui --locked body_maps_reasoning_effort_to_adaptive_thinking_and_effort
  • cargo test -p codewhale-tui --bin codewhale-tui --locked sse_fixture_decodes_text_thinking_signature_and_tool_use
  • cargo test -p codewhale-tui --bin codewhale-tui --locked parse_usage_reads_deepseek_cache_and_reasoning_tokens
  • cargo test -p codewhale-tui --bin codewhale-tui --locked api_provider_metadata_helpers_follow_config_provider_metadata
  • cargo test -p codewhale-tui --bin codewhale-tui --locked provider_config_key_follows_config_provider_metadata
  • node web/scripts/derive-facts.mjs
  • python3 scripts/check-provider-registry.py
  • ./scripts/release/check-versions.sh
  • ./scripts/release/check-ohos-deps.sh
  • git diff --check

Remaining Risk

This wires the documented API shape and adapter path, but does not prove account-specific server-tool behavior against a live DeepSeek key. That should stay as a follow-up manual probe before marking #2963 fully done.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new DeepseekAnthropic provider variant, allowing users to opt-in to a DeepSeek route that communicates via the Anthropic Messages wire protocol. The changes span configuration structures, provider registries, and TUI client implementations to support the new endpoint, authentication headers, and aliases. The review feedback highlights three key areas for improvement: a security risk where ANTHROPIC_API_KEY could be leaked to DeepSeek servers if configured as a fallback, missing hyphenated aliases in the subagent provider key matching logic, and an inconsistency in EnvRuntimeOverrides that prevents overriding the base URL for the new provider via environment variables.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +161 to +163
fn env_vars(&self) -> &'static [&'static str] {
&["DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY"]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Including ANTHROPIC_API_KEY in the list of environment variables for DeepseekAnthropic poses a security risk. If a user has ANTHROPIC_API_KEY set in their environment (which is common for Anthropic users) but has not set DEEPSEEK_API_KEY, the application will fall back to using the Anthropic API key and send it to DeepSeek's servers (https://api.deepseek.com/anthropic). This results in credential exposure to a third party. Since DeepSeek's Anthropic-compatible endpoint requires a DeepSeek API key and cannot authenticate with an Anthropic key, ANTHROPIC_API_KEY should be removed from this list.

Suggested change
fn env_vars(&self) -> &'static [&'static str] {
&["DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY"]
}
fn env_vars(&self) -> &'static [&'static str] {
&["DEEPSEEK_API_KEY"]
}

Comment thread crates/tui/src/config.rs
Comment on lines +469 to +472
ApiProvider::DeepseekAnthropic => matches!(
normalized.as_str(),
"deepseek_anthropic" | "deepseek_claude" | "deepseek_anthropic_api"
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The subagent provider key matching is missing the hyphenated aliases (deepseek-anthropic and deepseek-claude), which are supported elsewhere in the configuration (e.g., in Serde aliases). If a user configures a subagent using the hyphenated format, it will fail to match. We should include the hyphenated versions to ensure consistency and robustness.

Suggested change
ApiProvider::DeepseekAnthropic => matches!(
normalized.as_str(),
"deepseek_anthropic" | "deepseek_claude" | "deepseek_anthropic_api"
),
ApiProvider::DeepseekAnthropic => matches!(
normalized.as_str(),
"deepseek_anthropic" | "deepseek-anthropic" | "deepseek_claude" | "deepseek-claude" | "deepseek_anthropic_api"
),

Comment thread crates/config/src/lib.rs Outdated
// values (`providers.<name>.base_url`) still win when env is unset.
match provider {
ProviderKind::Deepseek => self.deepseek_base_url.clone(),
ProviderKind::DeepseekAnthropic => None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning None here means that EnvRuntimeOverrides does not support overriding the base URL for the deepseek-anthropic provider via environment variables, unlike other providers (e.g., Deepseek uses self.deepseek_base_url.clone()). This creates an inconsistency between crates/config and crates/tui (where DEEPSEEK_ANTHROPIC_BASE_URL or similar is handled). Consider adding a deepseek_anthropic_base_url field to EnvRuntimeOverrides to support environment-based base URL overrides consistently.

@Hmbown Hmbown force-pushed the issue/2963-deepseek-anthropic-route branch from c63864c to 69d198f Compare June 23, 2026 05:07

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@Hmbown Hmbown force-pushed the issue/2963-deepseek-anthropic-route branch from 69d198f to e93cc8f Compare June 23, 2026 05:12

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@Hmbown Hmbown marked this pull request as ready for review June 23, 2026 05:55

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@Hmbown Hmbown force-pushed the issue/2963-deepseek-anthropic-route branch from e93cc8f to 668e296 Compare June 23, 2026 17:02
@claude

claude Bot commented Jun 23, 2026

Copy link
Copy Markdown

Claude finished @Hmbown's task in 4m 27s —— View job


Code Review — DeepSeek Anthropic-compatible route

  • Gather context and read changed files
  • Review crates/config/src/provider.rs — provider metadata, env vars, aliases
  • Review crates/config/src/lib.rs — EnvRuntimeOverrides
  • Review crates/tui/src/client.rs — routing, header dialect
  • Review crates/tui/src/config.rs — subagent key matching, capability
  • Review remaining files (tests, docs, scripts)
  • Summarize findings

Summary

The core routing is well-structured: api_provider_uses_anthropic_messages predicate correctly gates both create_message / create_message_stream, the x-api-key + anthropic-version header dialect, and the reasoning-effort no-op. URL construction for the messages endpoint (https://api.deepseek.com/anthropic/v1/messages) is correct. Three Gemini findings are false positives in the current code. Two correctness bugs and one footgun need attention.


Correctness Bugs

1. translate() uses the wrong endpoint for DeepseekAnthropic — HIGH

crates/tui/src/client.rstranslate() always calls api_url_with_suffix(..., "chat/completions", ...) regardless of provider. For DeepseekAnthropic with base URL https://api.deepseek.com/anthropic, this resolves to https://api.deepseek.com/anthropic/v1/chat/completions, which does not exist on DeepSeek's Anthropic-compatible path. Compare create_message and create_message_stream, which both guard with:

if api_provider_uses_anthropic_messages(self.api_provider) {
    return self.handle_anthropic_message(request).await;
}

translate is missing an equivalent guard. A user who enables translation with deepseek-anthropic as the active provider will hit an HTTP error on every translation call. Fix this →


2. health_check() and maybe_probe_recovery() probe a non-existent endpoint — MEDIUM

Both methods call api_url(&self.base_url, "models"). For DeepseekAnthropic with base URL https://api.deepseek.com/anthropic, this resolves to https://api.deepseek.com/anthropic/v1/models — a path not documented in DeepSeek's Anthropic-compatible API. If this returns a non-2xx status (which is likely), mark_request_failure is called and the connection enters a degraded state. Once degraded, maybe_probe_recovery fires on every request and keeps probing the same wrong URL, keeping the connection permanently degraded.

The existing Anthropic provider has the same exposure (Anthropic's API does expose GET /v1/models, and DeepSeek may or may not mirror it on the /anthropic path). Worth adding an explicit health-check path for api_provider_uses_anthropic_messages providers that either skips the check or hits /v1/messages with a minimal probe body. Fix this →


3. fim_completion() has no guard for DeepseekAnthropic — LOW

fim_completion calls the beta/completions FIM endpoint:

let url = api_url_with_suffix(&self.base_url, "beta/completions", None);

For DeepseekAnthropic this hits https://api.deepseek.com/beta/completions, not the Anthropic path at all. The method should bail! early with a clear message if api_provider_uses_anthropic_messages(self.api_provider).


Minor Issues

4. TUI ProvidersConfig missing deepseekAnthropic camelCase alias

crates/config/src/lib.rs:325-330:

alias = "deepseek-anthropic",
alias = "deepseekAnthropic",   // ← present in crates/config
alias = "deepseek-claude",
alias = "deepseek_claude"

crates/tui/src/config.rs:2663-2668:

alias = "deepseek-anthropic",
// ← deepseekAnthropic is missing in crates/tui
alias = "deepseek-claude",
alias = "deepseek_claude"

Low impact (camelCase TOML keys are uncommon), but the two config structs should stay in sync.


Gemini False Positives

All three findings flagged by Gemini are not present in the current code:

  • ANTHROPIC_API_KEY in env_varsDeepseekAnthropic.env_vars() is already &["DEEPSEEK_API_KEY"] only (provider.rs:163-165). No leakage risk.

  • Missing hyphenated aliases in subagent_provider_key_matchesnormalize_subagent_provider_key maps both - and _ to _, so "deepseek-anthropic" normalizes to "deepseek_anthropic" before the matches! check. The hyphenated forms are already handled via the normalization path at config.rs:488-496.

  • EnvRuntimeOverrides base URL gapdeepseek_anthropic_base_url is a struct field (lib.rs:4307) loaded from DEEPSEEK_ANTHROPIC_BASE_URL / DEEPSEEK_CLAUDE_BASE_URL (lib.rs:4429-4432) and wired into base_url_for (lib.rs:4583). The override path is fully implemented.


Good Choices

  • Re-using handle_anthropic_message / handle_anthropic_stream through api_provider_uses_anthropic_messages is clean and avoids protocol-specific hacks in the shared dispatch path.
  • anthropic_messages_url("https://api.deepseek.com/anthropic") correctly produces https://api.deepseek.com/anthropic/v1/messages, matching DeepSeek's documented Anthropic-compatible path.
  • Auth header routing — x-api-key + anthropic-version applied via the existing predicate, and the ANTHROPIC_API_KEY / DEEPSEEK_API_KEY separation in env_vars is correct.
  • wire_model_for_provider for DeepseekAnthropic strips the deepseek-ai/ or deepseek/ namespace prefix, so the wire model sent to DeepSeek is the bare deepseek-v4-pro the Anthropic-compatible endpoint expects.
  • Scoping DeepSeek-native cache telemetry to existing routes while marking DeepseekAnthropic as AnthropicMessages in the capability matrix is the right call.

Add an opt-in deepseek-anthropic provider that resolves to DeepSeek's Anthropic-compatible endpoint and model defaults while leaving the existing DeepSeek chat-completions route unchanged.

Route the provider through the existing Anthropic Messages adapter, x-api-key header dialect, and TUI provider config table, with runtime capability metadata marking it as AnthropicMessages.

Update the provider docs, provider registry drift checker, and web facts maps so the manual provider implementation stays covered by generated guards.

Verified with: cargo fmt --all -- --check; cargo test -p codewhale-config --locked provider_metadata_defaults_match_runtime_helpers; cargo test -p codewhale-config --locked deepseek_anthropic_route_defaults_to_anthropic_endpoint; cargo test -p codewhale-tui --bin codewhale-tui --locked messages_url_tolerates_v1_suffix; cargo test -p codewhale-tui --bin codewhale-tui --locked deepseek_anthropic_uses_anthropic_header_dialect; cargo test -p codewhale-tui --bin codewhale-tui --locked provider_capability_deepseek_anthropic_uses_messages_payload; cargo test -p codewhale-tui --bin codewhale-tui --locked body_maps_reasoning_effort_to_adaptive_thinking_and_effort; cargo test -p codewhale-tui --bin codewhale-tui --locked sse_fixture_decodes_text_thinking_signature_and_tool_use; cargo test -p codewhale-tui --bin codewhale-tui --locked parse_usage_reads_deepseek_cache_and_reasoning_tokens; cargo test -p codewhale-tui --bin codewhale-tui --locked api_provider_metadata_helpers_follow_config_provider_metadata; cargo test -p codewhale-tui --bin codewhale-tui --locked provider_config_key_follows_config_provider_metadata; node web/scripts/derive-facts.mjs; python3 scripts/check-provider-registry.py; ./scripts/release/check-versions.sh; ./scripts/release/check-ohos-deps.sh; git diff --check
@Hmbown Hmbown force-pushed the issue/2963-deepseek-anthropic-route branch from 668e296 to abe866a Compare June 23, 2026 17:13
@claude

claude Bot commented Jun 23, 2026

Copy link
Copy Markdown

Claude finished @Hmbown's task in 3m 3s —— View job


Code Review — DeepSeek Anthropic-compatible route (updated)

  • Gather context and understand the PR
  • Read crates/tui/src/client.rs — check translate(), health_check(), fim_completion() guards
  • Read crates/tui/src/config.rs — check deepseekAnthropic alias
  • Read crates/config/src/provider.rs — verify env_vars, aliases
  • Summarize findings

Summary

All four correctness bugs from the first review pass are fixed. The implementation is clean and ready for merge. Details below.


All Prior Findings Resolved

1. translate() endpoint guard — FIXED

client.rs:949-954 now delegates to handle_anthropic_message when api_provider_uses_anthropic_messages is true, before falling through to the chat/completions path.

2. health_check() / maybe_probe_recovery() /models probe — FIXED

A new factored predicate api_provider_skips_models_probe (line 845-847) returns true for DeepseekAnthropic. Both health_check (line 1278) and maybe_probe_recovery (line 1143) guard on it, mark success, and return early — no HTTP call to the non-existent /anthropic/v1/models path.

3. fim_completion() guard — FIXED

client.rs:1696-1700 now bails with a clear diagnostic:

bail!("FIM completion is not supported for {} because it uses the Anthropic Messages protocol", ...)

4. deepseekAnthropic camelCase alias — FIXED

config.rs:2663-2666 now has all four aliases in sync with crates/config:

alias = "deepseek-anthropic",
alias = "deepseekAnthropic",
alias = "deepseek-claude",
alias = "deepseek_claude"

Gemini False Positives — Confirmed No-ops

  • ANTHROPIC_API_KEY in env_varsprovider.rs:163-165 is &["DEEPSEEK_API_KEY"] only. No leakage risk.
  • Missing hyphenated aliases in subagent_provider_key_matchesnormalize_subagent_provider_key maps - to _ before the matches! check, so "deepseek-anthropic""deepseek_anthropic" which matches. No change needed.
  • EnvRuntimeOverrides base URL gapdeepseek_anthropic_base_url field at lib.rs:4307 is loaded from DEEPSEEK_ANTHROPIC_BASE_URL / DEEPSEEK_CLAUDE_BASE_URL (line 4429-4432) and wired into base_url_for. Fully implemented.

Good Choices

  • Factoring api_provider_skips_models_probe as a separate predicate from api_provider_uses_anthropic_messages keeps the semantics clean — skipping the /models probe is specific to DeepseekAnthropic (Anthropic itself exposes GET /v1/models), while the Anthropic Messages wire protocol is shared by both.
  • translation_message_request passes reasoning_effort: Some("off") which the Anthropic adapter no-ops correctly (line 113: "off" | "disabled" | "none" | "false" => {}) — thinking block stays absent for translation.
  • model_completion_names_for_provider grouping DeepseekAnthropic with native DeepSeek variants (config.rs:1226-1228) is correct — same model catalog, different wire dialect.
  • wire_model_for_provider strips deepseek-ai/ / deepseek/ namespace prefixes for DeepseekAnthropic via the shared canonical_official_deepseek_model_id path (config.rs:1177-1191), so the wire model sent to DeepSeek is the bare deepseek-v4-pro the Anthropic-compatible endpoint expects.

@Hmbown Hmbown merged commit 5b8a5ac into main Jun 23, 2026
17 checks passed
@Hmbown Hmbown deleted the issue/2963-deepseek-anthropic-route branch June 23, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant